<!DOCTYPE html>
<html lang="zh-cn">
<head>
  <title>多进程研发模式增强 - 为企业级框架和应用而生</title>
  <meta charset="utf-8">
  <meta name="description" content="index.description">
  <meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
  <link rel="icon" href="/images/favicon.png" type="image/x-icon">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/docsearch.js/2/docsearch.min.css" />
<link rel="stylesheet" href="/css/index.css">

    <script>
    !function(t,e,a,r,c){t.TracertCmdCache=t.TracertCmdCache||[],t[c]=window[c]||
      {_isInit:!0,call:function(){t.TracertCmdCache.push(arguments)},
      start:function(t){this.call('start',t)}},t[c].l=new Date;
      var n=e.createElement(a),s=e.getElementsByTagName(a)[0];
      n.async=!0,n.src=r,s.parentNode.insertBefore(n,s)}
    (window,document,'script','https://tracert.alipay.com/tracert.js','Tracert');
      Tracert.start({
        plugins: [ 'BucName' ],
        spmAPos: 'a454',
        spmBPos: 'b4893',
      });
    </script>
  
</head>
<body>
  <div class="nav" >
  <header>
    <a href="/zh-cn/" class="nav-logo leftpadding" alt="egg"><img src="https://zos.alipayobjects.com/rmsportal/VTcUYAaoKqXyHJbLAPyF.svg"></a>
    <ul class="nav-item">
      <li>
        <form id="search-form">
          <input type="text" id="search-query" class="search-query st-default-search-input">
        </form>
      </li>
      <li><a href="/zh-cn/intro/" alt="指南">指南</a></li><li><a href="/api/" alt="API">API</a></li><li><a href="/zh-cn/tutorials/index.html" alt="教程">教程</a></li><li><a href="https://github.com/search?q=topic%3Aegg-plugin&type=Repositories" alt="插件">插件</a></li><li><a href="https://github.com/eggjs/egg/releases" alt="发布日志">发布日志</a></li>
      
      
        <li class="translations">
          <a class="nav-link">Translations</a>
          <span class="arrow"></span><ul id="dropdownContent" class="dropdown-content"><li><a id="en" href="/en/advanced/cluster-client.html" >English</a></li><li><a id="zh-cn" href="/zh-cn/advanced/cluster-client.html" style="color: #22ab28">中文</a></li></ul>
        </li>
      
      <li><iframe src="https://ghbtns.com/github-btn.html?user=eggjs&repo=egg&type=star&count=true" frameborder="0" scrolling="0" width="150px" height="20px"></iframe></li>
    </ul>
    <a id="mobileTrigger" href="#" class="mobile-trigger">
      <ul>
        <li></li>
        <li></li>
        <li></li>
      </ul>
    </a>
  </header>
</div>
  <div id="container" class="container">
    <div class="page-main">
  <article class="markdown-body">
    <h1>多进程研发模式增强</h1>
    <p>在前面的<a href="../core/cluster-and-ipc.html">多进程模型章节</a>中，我们详细讲述了框架的多进程模型，其中适合使用 Agent 进程的有一类常见的场景：一些中间件客户端需要和服务器建立长连接，理论上一台服务器最好只建立一个长连接，但多进程模型会导致 n 倍（n = Worker 进程数）连接被创建。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">+--------+   +--------+</span><br><span class="line">| Client |   | Client |   ... n</span><br><span class="line">+--------+   +--------+</span><br><span class="line">    |  \     /   |</span><br><span class="line">    |    \ /     |        n * m 个链接</span><br><span class="line">    |    / \     |</span><br><span class="line">    |  /     \   |</span><br><span class="line">+--------+   +--------+</span><br><span class="line">| Server |   | Server |   ... m</span><br><span class="line">+--------+   +--------+</span><br></pre></td></tr></table></figure>
<p>为了尽可能的复用长连接（因为它们对于服务端来说是非常宝贵的资源），我们会把它放到 Agent 进程里维护，然后通过 messenger 将数据传递给各个 Worker。这种做法是可行的，但是往往需要写大量代码去封装接口和实现数据的传递，非常麻烦。</p>
<p>另外，通过 messenger 传递数据效率是比较低的，因为它会通过 Master 来做中转；万一 IPC 通道出现问题还可能将 Master 进程搞挂。</p>
<p>那么有没有更好的方法呢？答案是肯定的，我们提供一种新的模式来降低这类客户端封装的复杂度。通过建立 Agent 和 Worker 的 socket 直连跳过 Master 的中转。Agent 作为对外的门面维持多个 Worker 进程的共享连接。</p>
<h2 id="核心思想"><a class="markdown-anchor" href="#核心思想">#</a> 核心思想</h2>
<ul>
<li>受到 <a href="http://www.cs.wustl.edu/~schmidt/PDF/lf.pdf" target="_blank" rel="noopener">Leader/Follower</a> 模式的启发</li>
<li>客户端会被区分为两种角色：
<ul>
<li>Leader: 负责和远程服务端维持连接，对于同一类的客户端只有一个 Leader</li>
<li>Follower: 会将具体的操作委托给 Leader，常见的是订阅模型（让 Leader 和远程服务端交互，并等待其返回）。</li>
</ul>
</li>
<li>如何确定谁是 Leader，谁是 Follower 呢？有两种模式：
<ul>
<li>自由竞争模式：客户端启动的时候通过本地端口的争夺来确定 Leader。例如：大家都尝试监听 7777 端口，最后只会有一个实例抢占到，那它就变成 Leader，其余的都是 Follower。</li>
<li>强制指定模式：框架指定某一个 Leader，其余的就是 Follower</li>
</ul>
</li>
<li>框架里面我们采用的是强制指定模式，Leader 只能在 Agent 里面创建，这也符合我们对 Agent 的定位</li>
<li>框架启动的时候 Master 会随机选择一个可用的端口作为 Cluster Client 监听的通讯端口，并将它通过参数传递给 Agent 和 App Worker</li>
<li>Leader 和 Follower 之间通过 socket 直连（通过通讯端口），不再需要 Master 中转</li>
</ul>
<p>新的模式下，客户端的通信方式如下：</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line">             +-------+</span><br><span class="line">             | start |</span><br><span class="line">             +---+---+</span><br><span class="line">                 |</span><br><span class="line">        +--------+---------+</span><br><span class="line">      __| port competition |__</span><br><span class="line">win /   +------------------+  \ lose</span><br><span class="line">   /                           \</span><br><span class="line">+---------------+     tcp conn     +-------------------+</span><br><span class="line">| Leader(Agent) |<span class="xml"><span class="tag">&lt;<span class="name">----------------</span>&gt;</span>| Follower(Worker1) |</span></span><br><span class="line"><span class="xml">+---------------+                  +-------------------+</span></span><br><span class="line"><span class="xml">    |            \ tcp conn</span></span><br><span class="line"><span class="xml">    |             \</span></span><br><span class="line"><span class="xml">+--------+         +-------------------+</span></span><br><span class="line"><span class="xml">| Client |         | Follower(Worker2) |</span></span><br><span class="line"><span class="xml">+--------+         +-------------------+</span></span><br></pre></td></tr></table></figure>
<h2 id="客户端接口类型抽象"><a class="markdown-anchor" href="#客户端接口类型抽象">#</a> 客户端接口类型抽象</h2>
<p>我们将客户端接口抽象为下面两大类，这也是对客户端接口的一个规范，对于符合规范的客户端，我们可以自动将其包装为 Leader/Follower 模式</p>
<ul>
<li>订阅、发布类（subscribe / publish）
<ul>
<li><code>subscribe(info, listener)</code> 接口包含两个参数，第一个是订阅的信息，第二个是订阅的回调函数</li>
<li><code>publish(info)</code> 接口包含一个参数，就是订阅的信息</li>
</ul>
</li>
<li>调用类 (invoke)，支持 callback, promise 和 generator function 三种风格的接口，但是推荐使用 generator function。</li>
</ul>
<p>客户端示例</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> Base = <span class="built_in">require</span>(<span class="string">'sdk-base'</span>);</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Client</span> <span class="keyword">extends</span> <span class="title">Base</span> </span>&#123;</span><br><span class="line">  <span class="keyword">constructor</span>(options) &#123;</span><br><span class="line">    <span class="keyword">super</span>(options);</span><br><span class="line">    <span class="comment">// 在初始化成功以后记得 ready</span></span><br><span class="line">    <span class="keyword">this</span>.ready(<span class="literal">true</span>);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 订阅</span></span><br><span class="line"><span class="comment">   *</span></span><br><span class="line"><span class="comment">   * @param &#123;Object&#125; info - 订阅的信息（一个 JSON 对象，注意尽量不要包含 Function, Buffer, Date 这类属性）</span></span><br><span class="line"><span class="comment">   * @param &#123;Function&#125; listener - 监听的回调函数，接收一个参数就是监听到的结果对象</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  subscribe(info, listener) &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 发布</span></span><br><span class="line"><span class="comment">   *</span></span><br><span class="line"><span class="comment">   * @param &#123;Object&#125; info - 发布的信息，和上面 subscribe 的 info 类似</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  publish(info) &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 获取数据 (invoke)</span></span><br><span class="line"><span class="comment">   *</span></span><br><span class="line"><span class="comment">   * @param &#123;String&#125; id - id</span></span><br><span class="line"><span class="comment">   * @return &#123;Object&#125; result</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="keyword">async</span> getData(id) &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h2 id="异常处理"><a class="markdown-anchor" href="#异常处理">#</a> 异常处理</h2>
<ul>
<li>Leader 如果“死掉”会触发新一轮的端口争夺，争夺到端口的那个实例被推选为新的 Leader</li>
<li>为保证 Leader 和 Follower 之间的通道健康，需要引入定时心跳检查机制，如果 Follower 在固定时间内没有发送心跳包，那么 Leader 会将 Follower 主动断开，从而触发 Follower 的重新初始化</li>
</ul>
<h2 id="协议和调用时序"><a class="markdown-anchor" href="#协议和调用时序">#</a> 协议和调用时序</h2>
<p>Leader 和 Follower 通过下面的协议进行数据交换：</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="number">0</span>       <span class="number">1</span>       <span class="number">2</span>               <span class="number">4</span>                                                              <span class="number">12</span></span><br><span class="line">+-------+-------+---------------+---------------------------------------------------------------+</span><br><span class="line">|version|req/res|    reserved   |                          request id                           |</span><br><span class="line">+-------------------------------+-------------------------------+-------------------------------+</span><br><span class="line">|           timeout             |   connection object length    |   application object length   |</span><br><span class="line">+-------------------------------+---------------------------------------------------------------+</span><br><span class="line">|         conn object (<span class="built_in">JSON</span> format)  ...                    |            app object             |</span><br><span class="line">+-----------------------------------------------------------+                                   |</span><br><span class="line">|                                          ...                                                  |</span><br><span class="line">+-----------------------------------------------------------------------------------------------+</span><br></pre></td></tr></table></figure>
<ol>
<li>在通讯端口上 Leader 启动一个 Local Server，所有的 Leader/Follower 通讯都经过 Local Server</li>
<li>Follower 连接上 Local Server 后，首先发送一个 register channel 的 packet（引入 channel 的概念是为了区别不同类型的客户端）</li>
<li>Local Server 会将 Follower 分配给指定的 Leader（根据客户端类型进行配对）</li>
<li>Follower 向 Leader 发送订阅、发布请求，</li>
<li>Leader 在订阅数据变更时通过 subscribe result packet 通知 Follower</li>
<li>Follower 向 Leader 发送调用请求，Leader 收到后执行相应操作后返回结果</li>
</ol>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line">+----------+             +---------------+          +---------+</span><br><span class="line">| Follower |             |  Local Server |          |  Leader |</span><br><span class="line">+----------+             +---------------+          +---------+</span><br><span class="line">     |     register channel     |       assign to        |</span><br><span class="line">     + -----------------------&gt; |  --------------------&gt; |</span><br><span class="line">     |                          |                        |</span><br><span class="line">     |                                subscribe          |</span><br><span class="line">     + ------------------------------------------------&gt; |</span><br><span class="line">     |                                 publish           |</span><br><span class="line">     + ------------------------------------------------&gt; |</span><br><span class="line">     |                                                   |</span><br><span class="line">     |       subscribe result                            |</span><br><span class="line">     | <span class="xml"><span class="tag">&lt;<span class="name">------------------------------------------------</span> +</span></span></span><br><span class="line"><span class="xml">     |                                                   |</span></span><br><span class="line"><span class="xml">     |                                 invoke            |</span></span><br><span class="line"><span class="xml">     + ------------------------------------------------&gt; |</span></span><br><span class="line"><span class="xml">     |          invoke result                            |</span></span><br><span class="line"><span class="xml">     | <span class="tag">&lt;<span class="name">------------------------------------------------</span> +</span></span></span><br><span class="line"><span class="xml">     |                                                   |</span></span><br></pre></td></tr></table></figure>
<h2 id="具体的使用方法"><a class="markdown-anchor" href="#具体的使用方法">#</a> 具体的使用方法</h2>
<p>下面我用一个简单的例子，介绍在框架里面如何让一个客户端支持 Leader/Follower 模式</p>
<ul>
<li>第一步，我们的客户端最好是符合上面提到过的接口约定，例如：</li>
</ul>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// registry_client.js</span></span><br><span class="line"><span class="keyword">const</span> URL = <span class="built_in">require</span>(<span class="string">'url'</span>);</span><br><span class="line"><span class="keyword">const</span> Base = <span class="built_in">require</span>(<span class="string">'sdk-base'</span>);</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">RegistryClient</span> <span class="keyword">extends</span> <span class="title">Base</span> </span>&#123;</span><br><span class="line">  <span class="keyword">constructor</span>(options) &#123;</span><br><span class="line">    <span class="keyword">super</span>(&#123;</span><br><span class="line">      <span class="comment">// 指定异步启动的方法</span></span><br><span class="line">      initMethod: <span class="string">'init'</span>,</span><br><span class="line">    &#125;);</span><br><span class="line">    <span class="keyword">this</span>._options = options;</span><br><span class="line">    <span class="keyword">this</span>._registered = <span class="keyword">new</span> <span class="built_in">Map</span>();</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 启动逻辑</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="keyword">async</span> init() &#123;</span><br><span class="line">    <span class="keyword">this</span>.ready(<span class="literal">true</span>);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 获取配置</span></span><br><span class="line"><span class="comment">   * @param &#123;String&#125; dataId - the dataId</span></span><br><span class="line"><span class="comment">   * @return &#123;Object&#125; 配置</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="keyword">async</span> getConfig(dataId) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">this</span>._registered.get(dataId);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 订阅</span></span><br><span class="line"><span class="comment">   * @param &#123;Object&#125; reg</span></span><br><span class="line"><span class="comment">   *   - &#123;String&#125; dataId - the dataId</span></span><br><span class="line"><span class="comment">   * @param &#123;Function&#125;  listener - the listener</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  subscribe(reg, listener) &#123;</span><br><span class="line">    <span class="keyword">const</span> key = reg.dataId;</span><br><span class="line">    <span class="keyword">this</span>.on(key, listener);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> data = <span class="keyword">this</span>._registered.get(key);</span><br><span class="line">    <span class="keyword">if</span> (data) &#123;</span><br><span class="line">      process.nextTick(<span class="function"><span class="params">()</span> =&gt;</span> listener(data));</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 发布</span></span><br><span class="line"><span class="comment">   * @param &#123;Object&#125; reg</span></span><br><span class="line"><span class="comment">   *   - &#123;String&#125; dataId - the dataId</span></span><br><span class="line"><span class="comment">   *   - &#123;String&#125; publishData - the publish data</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  publish(reg) &#123;</span><br><span class="line">    <span class="keyword">const</span> key = reg.dataId;</span><br><span class="line">    <span class="keyword">let</span> changed = <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (<span class="keyword">this</span>._registered.has(key)) &#123;</span><br><span class="line">      <span class="keyword">const</span> arr = <span class="keyword">this</span>._registered.get(key);</span><br><span class="line">      <span class="keyword">if</span> (arr.indexOf(reg.publishData) === <span class="number">-1</span>) &#123;</span><br><span class="line">        changed = <span class="literal">true</span>;</span><br><span class="line">        arr.push(reg.publishData);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      changed = <span class="literal">true</span>;</span><br><span class="line">      <span class="keyword">this</span>._registered.set(key, [reg.publishData]);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (changed) &#123;</span><br><span class="line">      <span class="keyword">this</span>.emit(key, <span class="keyword">this</span>._registered.get(key).map(<span class="function"><span class="params">url</span> =&gt;</span> URL.parse(url, <span class="literal">true</span>)));</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">module</span>.exports = RegistryClient;</span><br></pre></td></tr></table></figure>
<ul>
<li>第二步，使用 <code>agent.cluster</code> 接口对 RegistryClient 进行封装</li>
</ul>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// agent.js</span></span><br><span class="line"><span class="keyword">const</span> RegistryClient = <span class="built_in">require</span>(<span class="string">'registry_client'</span>);</span><br><span class="line"></span><br><span class="line"><span class="built_in">module</span>.exports = <span class="function"><span class="params">agent</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="comment">// 对 RegistryClient 进行封装和实例化</span></span><br><span class="line">  agent.registryClient = agent.cluster(RegistryClient)</span><br><span class="line">    <span class="comment">// create 方法的参数就是 RegistryClient 构造函数的参数</span></span><br><span class="line">    .create(&#123;&#125;);</span><br><span class="line"></span><br><span class="line">  agent.beforeStart(<span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">    <span class="keyword">await</span> agent.registryClient.ready();</span><br><span class="line">    agent.coreLogger.info(<span class="string">'registry client is ready'</span>);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<ul>
<li>第三步，使用 <code>app.cluster</code> 接口对 RegistryClient 进行封装</li>
</ul>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app.js</span></span><br><span class="line"><span class="keyword">const</span> RegistryClient = <span class="built_in">require</span>(<span class="string">'registry_client'</span>);</span><br><span class="line"></span><br><span class="line"><span class="built_in">module</span>.exports = <span class="function"><span class="params">app</span> =&gt;</span> &#123;</span><br><span class="line">  app.registryClient = app.cluster(RegistryClient).create(&#123;&#125;);</span><br><span class="line">  app.beforeStart(<span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">    <span class="keyword">await</span> app.registryClient.ready();</span><br><span class="line">    app.coreLogger.info(<span class="string">'registry client is ready'</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 调用 subscribe 进行订阅</span></span><br><span class="line">    app.registryClient.subscribe(&#123;</span><br><span class="line">      dataId: <span class="string">'demo.DemoService'</span>,</span><br><span class="line">    &#125;, val =&gt; &#123;</span><br><span class="line">      <span class="comment">// ...</span></span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 调用 publish 发布数据</span></span><br><span class="line">    app.registryClient.publish(&#123;</span><br><span class="line">      dataId: <span class="string">'demo.DemoService'</span>,</span><br><span class="line">      publishData: <span class="string">'xxx'</span>,</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 调用 getConfig 接口</span></span><br><span class="line">    <span class="keyword">const</span> res = <span class="keyword">await</span> app.registryClient.getConfig(<span class="string">'demo.DemoService'</span>);</span><br><span class="line">    <span class="built_in">console</span>.log(res);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<p>是不是很简单？</p>
<p>当然，如果你的客户端不是那么『标准』，那你可能需要用到其他一些 API，比如，你的订阅函数不叫 subscribe，叫 sub</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">MockClient</span> <span class="keyword">extends</span> <span class="title">Base</span> </span>&#123;</span><br><span class="line">  <span class="keyword">constructor</span>(options) &#123;</span><br><span class="line">    <span class="keyword">super</span>(&#123;</span><br><span class="line">      initMethod: <span class="string">'init'</span>,</span><br><span class="line">    &#125;);</span><br><span class="line">    <span class="keyword">this</span>._options = options;</span><br><span class="line">    <span class="keyword">this</span>._registered = <span class="keyword">new</span> <span class="built_in">Map</span>();</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">async</span> init() &#123;</span><br><span class="line">    <span class="keyword">this</span>.ready(<span class="literal">true</span>);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  sub(info, listener) &#123;</span><br><span class="line">    <span class="keyword">const</span> key = reg.dataId;</span><br><span class="line">    <span class="keyword">this</span>.on(key, listener);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> data = <span class="keyword">this</span>._registered.get(key);</span><br><span class="line">    <span class="keyword">if</span> (data) &#123;</span><br><span class="line">      process.nextTick(<span class="function"><span class="params">()</span> =&gt;</span> listener(data));</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>你需要用 delegate API 手动设置</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// agent.js</span></span><br><span class="line"><span class="built_in">module</span>.exports = <span class="function"><span class="params">agent</span> =&gt;</span> &#123;</span><br><span class="line">  agent.mockClient = agent.cluster(MockClient)</span><br><span class="line">    <span class="comment">// 将 sub 代理到 subscribe 逻辑上</span></span><br><span class="line">    .delegate(<span class="string">'sub'</span>, <span class="string">'subscribe'</span>)</span><br><span class="line">    .create();</span><br><span class="line"></span><br><span class="line">  agent.beforeStart(<span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">    <span class="keyword">await</span> agent.mockClient.ready();</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app.js</span></span><br><span class="line"><span class="built_in">module</span>.exports = <span class="function"><span class="params">app</span> =&gt;</span> &#123;</span><br><span class="line">  app.mockClient = app.cluster(MockClient)</span><br><span class="line">    <span class="comment">// 将 sub 代理到 subscribe 逻辑上</span></span><br><span class="line">    .delegate(<span class="string">'sub'</span>, <span class="string">'subscribe'</span>)</span><br><span class="line">    .create();</span><br><span class="line"></span><br><span class="line">  app.beforeStart(<span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">    <span class="keyword">await</span> app.mockClient.ready();</span><br><span class="line"></span><br><span class="line">    app.sub(&#123; <span class="attr">id</span>: <span class="string">'test-id'</span> &#125;, val =&gt; &#123;</span><br><span class="line">      <span class="comment">// put your code here</span></span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<p>我们已经理解，通过 cluster-client 可以让我们在不理解多进程模型的情况下开发『纯粹』的 RegistryClient，只负责和服务端进行交互，然后使用 cluster-client 进行简单的 wrap 就可以得到一个支持多进程模型的 ClusterClient。这里的 RegistryClient 实际上是一个专门负责和远程服务通信进行数据通信的 DataClient。</p>
<p>大家可能已经发现，ClusterClient 同时带来了一些约束，如果想在各进程暴露同样的方法，那么 RegistryClient 上只能支持 sub/pub 模式以及异步的 API 调用。因为在多进程模型中所有的交互都必须经过 socket 通信，势必带来了这一约束。</p>
<p>假设我们要实现一个同步的 get 方法，subscribe 过的数据直接放入内存，使用 get 方法时直接返回。要怎么实现呢？而真实情况可能比这更复杂。</p>
<p>在这里，我们引入一个 APIClient 的最佳实践。对于有读取缓存数据等同步 API 需求的模块，在 RegistryClient 基础上再封装一个 APIClient 来实现这些与远程服务端交互无关的 API，暴露给用户使用到的是这个 APIClient 的实例。</p>
<p>在 APIClient 内部实现上：</p>
<ul>
<li>异步数据获取，通过调用基于 ClusterClient 的 RegistryClient 的 API 实现。</li>
<li>同步调用等与服务端无关的接口在 APIClient 上实现。由于 ClusterClient 的 API 已经抹平了多进程差异，所以在开发 APIClient 调用到 RegistryClient 时也无需关心多进程模型。</li>
</ul>
<p>例如在模块的 APIClient 中增加带缓存的 get 同步方法：</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// some-client/index.js</span></span><br><span class="line"><span class="keyword">const</span> cluster = <span class="built_in">require</span>(<span class="string">'cluster-client'</span>);</span><br><span class="line"><span class="keyword">const</span> RegistryClient = <span class="built_in">require</span>(<span class="string">'./registry_client'</span>);</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">APIClient</span> <span class="keyword">extends</span> <span class="title">Base</span> </span>&#123;</span><br><span class="line">  <span class="keyword">constructor</span>(options) &#123;</span><br><span class="line">    <span class="keyword">super</span>(options);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// options.cluster 用于给 Egg 的插件传递 app.cluster 进来</span></span><br><span class="line">    <span class="keyword">this</span>._client = (options.cluster || cluster)(RegistryClient).create(options);</span><br><span class="line">    <span class="keyword">this</span>._client.ready(<span class="function"><span class="params">()</span> =&gt;</span> <span class="keyword">this</span>.ready(<span class="literal">true</span>));</span><br><span class="line"></span><br><span class="line">    <span class="keyword">this</span>._cache = &#123;&#125;;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// subMap:</span></span><br><span class="line">    <span class="comment">// &#123;</span></span><br><span class="line">    <span class="comment">//   foo: reg1,</span></span><br><span class="line">    <span class="comment">//   bar: reg2,</span></span><br><span class="line">    <span class="comment">// &#125;</span></span><br><span class="line">    <span class="keyword">const</span> subMap = options.subMap;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">const</span> key <span class="keyword">in</span> subMap) &#123;</span><br><span class="line">      <span class="keyword">this</span>.subscribe(subMap[key], value =&gt; &#123;</span><br><span class="line">        <span class="keyword">this</span>._cache[key] = value;</span><br><span class="line">      &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  subscribe(reg, listener) &#123;</span><br><span class="line">    <span class="keyword">this</span>._client.subscribe(reg, listener);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  publish(reg) &#123;</span><br><span class="line">    <span class="keyword">this</span>._client.publish(reg);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  get(key) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">this</span>._cache[key];</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 最终模块向外暴露这个 APIClient</span></span><br><span class="line"><span class="built_in">module</span>.exports = APIClient;</span><br></pre></td></tr></table></figure>
<p>那么我们就可以这么使用该模块：</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app.js || agent.js</span></span><br><span class="line"><span class="keyword">const</span> APIClient = <span class="built_in">require</span>(<span class="string">'some-client'</span>); <span class="comment">// 上面那个模块</span></span><br><span class="line"><span class="built_in">module</span>.exports = <span class="function"><span class="params">app</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> config = app.config.apiClient;</span><br><span class="line">  app.apiClient = <span class="keyword">new</span> APIClient(<span class="built_in">Object</span>.assign(&#123;&#125;, config, &#123; <span class="attr">cluster</span>: app.cluster &#125;);</span><br><span class="line">  app.beforeStart(<span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">    <span class="keyword">await</span> app.apiClient.ready();</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// config.$&#123;env&#125;.js</span></span><br><span class="line">exports.apiClient = &#123;</span><br><span class="line">  subMap: &#123;</span><br><span class="line">    foo: &#123;</span><br><span class="line">      id: <span class="string">''</span>,</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="comment">// bar...</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<p>为了方便你封装 <code>APIClient</code>，在 <a href="https://www.npmjs.com/package/cluster-client" target="_blank" rel="noopener">cluster-client</a> 模块中提供了一个 <code>APIClientBase</code> 基类，那么上面的 <code>APIClient</code> 可以改写为：</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> APIClientBase = <span class="built_in">require</span>(<span class="string">'cluster-client'</span>).APIClientBase;</span><br><span class="line"><span class="keyword">const</span> RegistryClient = <span class="built_in">require</span>(<span class="string">'./registry_client'</span>);</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">APIClient</span> <span class="keyword">extends</span> <span class="title">APIClientBase</span> </span>&#123;</span><br><span class="line">  <span class="comment">// 返回原始的客户端类</span></span><br><span class="line">  get DataClient() &#123;</span><br><span class="line">    <span class="keyword">return</span> RegistryClient;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 用于设置 cluster-client 相关参数，等同于 cluster 方法的第二个参数</span></span><br><span class="line">  get clusterOptions() &#123;</span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">      responseTimeout: <span class="number">120</span> * <span class="number">1000</span>,</span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  subscribe(reg, listener) &#123;</span><br><span class="line">    <span class="keyword">this</span>._client.subscribe(reg, listener);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  publish(reg) &#123;</span><br><span class="line">    <span class="keyword">this</span>._client.publish(reg);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  get(key) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">this</span>._cache[key];</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>总结一下：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">+------------------------------------------------+</span><br><span class="line">| APIClient                                      |</span><br><span class="line">|       +----------------------------------------|</span><br><span class="line">|       | ClusterClient                          |</span><br><span class="line">|       |      +---------------------------------|</span><br><span class="line">|       |      | RegistryClient                  |</span><br><span class="line">+------------------------------------------------+</span><br></pre></td></tr></table></figure>
<ul>
<li>RegistryClient - 负责和远端服务通讯，实现数据的存取，只支持异步 API，不关心多进程模型。</li>
<li>ClusterClient - 通过 cluster-client 模块进行简单 wrap 得到的 client 实例，负责自动抹平多进程模型的差异。</li>
<li>APIClient - 内部调用 ClusterClient 做数据同步，无需关心多进程模型，用户最终使用的模块。API 都通过此处暴露，支持同步和异步。</li>
</ul>
<p>有兴趣的同学可以看一下<a href="https://github.com/eggjs/egg/issues/322" target="_blank" rel="noopener">增强多进程研发模式</a> 讨论过程。</p>
<h2 id="在框架里面-cluster-client-相关的配置项"><a class="markdown-anchor" href="#在框架里面-cluster-client-相关的配置项">#</a> 在框架里面 cluster-client 相关的配置项</h2>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * @property &#123;Number&#125; responseTimeout - response timeout, default is 60000</span></span><br><span class="line"><span class="comment"> * @property &#123;Transcode&#125; [transcode]</span></span><br><span class="line"><span class="comment"> *   - &#123;Function&#125; encode - custom serialize method</span></span><br><span class="line"><span class="comment"> *   - &#123;Function&#125; decode - custom deserialize method</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line">config.clusterClient = &#123;</span><br><span class="line">  responseTimeout: <span class="number">60000</span>,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<table>
<thead>
<tr>
<th>配置项</th>
<th>类型</th>
<th>默认值</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td>responseTimeout</td>
<td>number</td>
<td>60000 （一分钟）</td>
<td>全局的进程间通讯的超时时长，不能设置的太短，因为代理的接口本身也有超时设置</td>
</tr>
<tr>
<td>transcode</td>
<td>function</td>
<td>N/A</td>
<td>进程间通讯的序列化方式，默认采用 <a href="https://www.npmjs.com/package/serialize-json" target="_blank" rel="noopener">serialize-json</a>（建议不要自行设置）</td>
</tr>
</tbody>
</table>
<p>上面是全局的配置方式。如果，你想对一个客户端单独做设置</p>
<ul>
<li>可以通过 <code>app/agent.cluster(ClientClass, options)</code> 的第二个参数 <code>options</code> 进行覆盖</li>
</ul>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line">app.registryClient = app.cluster(RegistryClient, &#123;</span><br><span class="line">  responseTimeout: <span class="number">120</span> * <span class="number">1000</span>, <span class="comment">// 这里传入的是和 cluster-client 相关的参数</span></span><br><span class="line">&#125;).create(&#123;</span><br><span class="line">  <span class="comment">// 这里传入的是 RegistryClient 需要的参数</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<ul>
<li>也可以通过覆盖 <code>APIClientBase</code> 的 <code>clusterOptions</code> 这个 <code>getter</code> 属性</li>
</ul>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> APIClientBase = <span class="built_in">require</span>(<span class="string">'cluster-client'</span>).APIClientBase;</span><br><span class="line"><span class="keyword">const</span> RegistryClient = <span class="built_in">require</span>(<span class="string">'./registry_client'</span>);</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">APIClient</span> <span class="keyword">extends</span> <span class="title">APIClientBase</span> </span>&#123;</span><br><span class="line">  get DataClient() &#123;</span><br><span class="line">    <span class="keyword">return</span> RegistryClient;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  get clusterOptions() &#123;</span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">      responseTimeout: <span class="number">120</span> * <span class="number">1000</span>,</span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">module</span>.exports = APIClient;</span><br></pre></td></tr></table></figure>

  </article>
  <aside id="mobileAside" class="toc">
  <div class="mobile-menu">
    <ul>
      <li><a href="/zh-cn/intro/" alt="指南">指南</a></li><li><a href="/api/" alt="API">API</a></li><li><a href="/zh-cn/tutorials/index.html" alt="教程">教程</a></li><li><a href="https://github.com/search?q=topic%3Aegg-plugin&type=Repositories" alt="插件">插件</a></li><li><a href="https://github.com/eggjs/egg/releases" alt="发布日志">发布日志</a></li>
      
      
        <li class="translations">
          <a class="nav-link">Translations</a>
          <span class="arrow"></span><ul id="dropdownContent" class="dropdown-content"><li><a id="en" href="/en/advanced/cluster-client.html" >English</a></li><li><a id="zh-cn" href="/zh-cn/advanced/cluster-client.html" style="color: #22ab28">中文</a></li></ul>
        </li>
      
    </ul>
  </div>
  <dl><dt id="title-Intro" style="cursor: pointer;" class="aside-title">新手指南<a id="collapse-icon-Intro" class="icon opend"></a></dt><dd id=panel-Intro><ul><li><a href="/zh-cn/intro/index.html" class="menu-link">Egg.js 是什么?</a></li><li><a href="/zh-cn/intro/egg-and-koa.html" class="menu-link">Egg.js 和 Koa</a></li><li><a href="/zh-cn/intro/quickstart.html" class="menu-link">快速入门</a></li><li><a href="/zh-cn/tutorials/progressive.html" class="menu-link">渐进式开发</a></li><li><a href="/zh-cn/migration.html" class="menu-link">2.x 升级指南</a></li></ul></dd><dt id="title-Basics" style="cursor: pointer;" class="aside-title">基础功能<a id="collapse-icon-Basics" class="icon opend"></a></dt><dd id=panel-Basics><ul><li><a href="/zh-cn/basics/structure.html" class="menu-link">目录结构</a></li><li><a href="/zh-cn/basics/objects.html" class="menu-link">内置对象</a></li><li><a href="/zh-cn/basics/env.html" class="menu-link">运行环境</a></li><li><a href="/zh-cn/basics/config.html" class="menu-link">配置</a></li><li><a href="/zh-cn/basics/middleware.html" class="menu-link">中间件</a></li><li><a href="/zh-cn/basics/router.html" class="menu-link">Router</a></li><li><a href="/zh-cn/basics/controller.html" class="menu-link">Controller</a></li><li><a href="/zh-cn/basics/service.html" class="menu-link">Service</a></li><li><a href="/zh-cn/basics/plugin.html" class="menu-link">插件</a></li><li><a href="/zh-cn/basics/schedule.html" class="menu-link">定时任务</a></li><li><a href="/zh-cn/basics/extend.html" class="menu-link">框架扩展</a></li><li><a href="/zh-cn/basics/app-start.html" class="menu-link">启动自定义</a></li></ul></dd><dt id="title-Core" style="cursor: pointer;" class="aside-title">核心功能<a id="collapse-icon-Core" class="icon opend"></a></dt><dd id=panel-Core><ul><li><a href="/zh-cn/core/development.html" class="menu-link">本地开发</a></li><li><a href="/zh-cn/core/unittest.html" class="menu-link">单元测试</a></li><li><a href="/zh-cn/core/deployment.html" class="menu-link">应用部署</a></li><li><a href="/zh-cn/core/logger.html" class="menu-link">日志</a></li><li><a href="/zh-cn/core/httpclient.html" class="menu-link">HttpClient</a></li><li><a href="/zh-cn/core/cookie-and-session.html" class="menu-link">Cookie and Session</a></li><li><a href="/zh-cn/core/cluster-and-ipc.html" class="menu-link">多进程模型和进程间通讯</a></li><li><a href="/zh-cn/core/view.html" class="menu-link">模板渲染</a></li><li><a href="/zh-cn/core/error-handling.html" class="menu-link">异常处理</a></li><li><a href="/zh-cn/core/security.html" class="menu-link">安全</a></li><li><a href="/zh-cn/core/i18n.html" class="menu-link">国际化</a></li></ul></dd><dt id="title-Tutorials" style="cursor: pointer;" class="aside-title">教程<a id="collapse-icon-Tutorials" class="icon opend"></a></dt><dd id=panel-Tutorials><ul><li><a href="/zh-cn/tutorials/mysql.html" class="menu-link">MySQL</a></li><li><a href="/zh-cn/tutorials/restful.html" class="menu-link">RESTful API</a></li><li><a href="/zh-cn/tutorials/passport.html" class="menu-link">Passport 鉴权</a></li><li><a href="/zh-cn/tutorials/socketio.html" class="menu-link">Socket.IO</a></li><li><a href="/zh-cn/tutorials/assets.html" class="menu-link">静态资源</a></li><li><a href="/zh-cn/tutorials/typescript.html" class="menu-link">TypeScript</a></li></ul></dd><dt id="title-Advanced" style="cursor: pointer;" class="aside-title">进阶<a id="collapse-icon-Advanced" class="icon opend"></a></dt><dd id=panel-Advanced><ul><li><a href="/zh-cn/advanced/loader.html" class="menu-link">Loader</a></li><li><a href="/zh-cn/advanced/plugin.html" class="menu-link">插件开发</a></li><li><a href="/zh-cn/advanced/framework.html" class="menu-link">框架开发</a></li><li><a href="/zh-cn/advanced/cluster-client.html" class="menu-link">多进程研发模式增强</a></li><li><a href="/zh-cn/advanced/view-plugin.html" class="menu-link">模板插件开发规范</a></li><li><a href="/zh-cn/style-guide.html" class="menu-link">代码风格指南</a></li></ul></dd><dt id="title-Community" style="cursor: pointer;" class="aside-title">社区<a id="collapse-icon-Community" class="icon opend"></a></dt><dd id=panel-Community><ul><li><a href="/zh-cn/plugins/" class="menu-link">内置插件列表</a></li><li><a href="/zh-cn/contributing.html" class="menu-link">如何贡献</a></li><li><a href="/zh-cn/resource.html" class="menu-link">资源</a></li><li><a href="/zh-cn/faq.html" class="menu-link">常见问题</a></li></ul></dd></dl>
</aside>
<script>
var mobileTrigger = document.getElementById('mobileTrigger');
var mobileAside = document.getElementById('mobileAside');

var expandMenu = function(title) {
  // handle icon
  const collapseIcon = document.getElementById('collapse-icon-' + title);
  if (collapseIcon) {
    collapseIcon.className = 'icon opend';
  }
  // handle panelEle
  const panelEle = document.getElementById('panel-' + title);
  if (panelEle) {
    panelEle.className = '';
  }
}

var collapseMenu = function(title) {
  // handle icon
  const collapseIcon = document.getElementById('collapse-icon-' + title);
  if (collapseIcon) {
    collapseIcon.className = 'icon closed';
  }
  // handle panelEle
  const panelEle = document.getElementById('panel-' + title);
  if (panelEle) {
    panelEle.className = 'aside-panel-hidden';
  }
}

mobileAside.onclick = function(e) {
  const targetId = e.target.id;
  if (targetId && (targetId.indexOf('title-') > -1 || targetId.indexOf('collapse-icon-') > -1)) {
    const title = targetId.replace('title-', '').replace('collapse-icon-', '');
    try { 
      // the the browser may have no localStroage or JSON.parse may throw exception.
      const menuInfo = JSON.parse(window.localStorage.getItem('menuInfo'));
        
      // current menu status
      const curClosed = menuInfo[title] ? menuInfo[title].closed : false; // default false

      // change UI
      curClosed ? expandMenu(title) : collapseMenu(title);

      // save menuInfo to localStorage
      menuInfo[title] = { closed: !curClosed } // opposite
      window.localStorage.setItem('menuInfo', JSON.stringify(menuInfo));
    } catch (e) {}
  }
};

mobileTrigger.onclick = function(e) {
  e.preventDefault();
  if (mobileAside.className.indexOf('mobile-show') === -1) {
    mobileAside.className += ' mobile-show';
  } else {
    mobileAside.className = 'toc';
  }
};

(function() {
  // save data to localStorage because the page will refresh when user change the url.
  let menuInfo;
  try { 
    // the the browser may have no localStroage or JSON.parse may throw exception.
    menuInfo = JSON.parse(window.localStorage.getItem('menuInfo'));
    if (!menuInfo) {
      menuInfo = {};
      window.localStorage.setItem('menuInfo', JSON.stringify(menuInfo));
    }
  } catch (e) {
    menuInfo = {}; // default {}
  }

  for (const title in menuInfo) {
    if (menuInfo[title] && menuInfo[title].closed) { // menu in closed status.
      collapseMenu(title);
    } else {
      expandMenu(title);
    }
  }

  // highlight menu
  const pathname = window.location.pathname;
  const selector = `a[href="${pathname}"].menu-link,a[href="${pathname}index.html"].menu-link`;
  const menuItem = mobileAside.querySelector(selector);
  if (menuItem) { menuItem.className += ' highlight'; }
})();
</script>

</div>

  </div>
</body>
<script src="https://cdn.jsdelivr.net/docsearch.js/2/docsearch.min.js"></script>
<script>
docsearch({
  apiKey: '1561de31a86f79507ea00cdb54ce647c',
  indexName: 'eggjs',
  inputSelector: '#search-query',
});
</script>
<div class="cnzz">
<script src="https://s11.cnzz.com/z_stat.php?id=1261142226&web_id=1261142226" language="JavaScript"></script>
</div>

</html>
